(function() {

    const _changeEvent = {
        type: 'change'
    };
    const _startEvent = {
        type: 'start'
    };
    const _endEvent = {
        type: 'end'
    };

    class TrackballControls extends THREE.EventDispatcher {

        constructor(object, domElement) {

            super();
            if (domElement === undefined) console.warn('THREE.TrackballControls: The second parameter "domElement" is now mandatory.');
            if (domElement === document) console.error('THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.');
            const scope = this;
            const STATE = {
                NONE: -1,
                ROTATE: 0,
                ZOOM: 1,
                PAN: 2,
                TOUCH_ROTATE: 3,
                TOUCH_ZOOM_PAN: 4
            };
            this.object = object;
            this.domElement = domElement;
            this.domElement.style.touchAction = 'none'; // disable touch scroll
            // API

            this.enabled = true;
            this.screen = {
                left: 0,
                top: 0,
                width: 0,
                height: 0
            };
            this.rotateSpeed = 1.0;
            this.zoomSpeed = 1.2;
            this.panSpeed = 0.3;
            this.noRotate = false;
            this.noZoom = false;
            this.noPan = false;
            this.staticMoving = false;
            this.dynamicDampingFactor = 0.2;
            this.minDistance = 0;
            this.maxDistance = Infinity;
            this.keys = ['KeyA',
                /*A*/
                'KeyS',
                /*S*/
                'KeyD'
                /*D*/
            ];
            this.mouseButtons = {
                LEFT: THREE.MOUSE.ROTATE,
                MIDDLE: THREE.MOUSE.DOLLY,
                RIGHT: THREE.MOUSE.PAN
            }; // internals

            this.target = new THREE.Vector3();
            const EPS = 0.000001;
            const lastPosition = new THREE.Vector3();
            let lastZoom = 1;
            let _state = STATE.NONE,
                _keyState = STATE.NONE,
                _touchZoomDistanceStart = 0,
                _touchZoomDistanceEnd = 0,
                _lastAngle = 0;

            const _eye = new THREE.Vector3(),
                _movePrev = new THREE.Vector2(),
                _moveCurr = new THREE.Vector2(),
                _lastAxis = new THREE.Vector3(),
                _zoomStart = new THREE.Vector2(),
                _zoomEnd = new THREE.Vector2(),
                _panStart = new THREE.Vector2(),
                _panEnd = new THREE.Vector2(),
                _pointers = [],
                _pointerPositions = {}; // for reset


            this.target0 = this.target.clone();
            this.position0 = this.object.position.clone();
            this.up0 = this.object.up.clone();
            this.zoom0 = this.object.zoom; // methods

            this.handleResize = function() {

                const box = scope.domElement.getBoundingClientRect(); // adjustments come from similar code in the jquery offset() function

                const d = scope.domElement.ownerDocument.documentElement;
                scope.screen.left = box.left + window.pageXOffset - d.clientLeft;
                scope.screen.top = box.top + window.pageYOffset - d.clientTop;
                scope.screen.width = box.width;
                scope.screen.height = box.height;

            };

            const getMouseOnScreen = function() {

                const vector = new THREE.Vector2();
                return function getMouseOnScreen(pageX, pageY) {

                    vector.set((pageX - scope.screen.left) / scope.screen.width, (pageY - scope.screen.top) / scope.screen.height);
                    return vector;

                };

            }();

            const getMouseOnCircle = function() {

                const vector = new THREE.Vector2();
                return function getMouseOnCircle(pageX, pageY) {

                    vector.set((pageX - scope.screen.width * 0.5 - scope.screen.left) / (scope.screen.width * 0.5), (scope.screen.height + 2 * (scope.screen.top - pageY)) / scope.screen.width // screen.width intentional
                    );
                    return vector;

                };

            }();

            this.rotateCamera = function() {

                const axis = new THREE.Vector3(),
                    quaternion = new THREE.Quaternion(),
                    eyeDirection = new THREE.Vector3(),
                    objectUpDirection = new THREE.Vector3(),
                    objectSidewaysDirection = new THREE.Vector3(),
                    moveDirection = new THREE.Vector3();
                return function rotateCamera() {

                    moveDirection.set(_moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0);
                    let angle = moveDirection.length();

                    if (angle) {

                        _eye.copy(scope.object.position).sub(scope.target);

                        eyeDirection.copy(_eye).normalize();
                        objectUpDirection.copy(scope.object.up).normalize();
                        objectSidewaysDirection.crossVectors(objectUpDirection, eyeDirection).normalize();
                        objectUpDirection.setLength(_moveCurr.y - _movePrev.y);
                        objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x);
                        moveDirection.copy(objectUpDirection.add(objectSidewaysDirection));
                        axis.crossVectors(moveDirection, _eye).normalize();
                        angle *= scope.rotateSpeed;
                        quaternion.setFromAxisAngle(axis, angle);

                        _eye.applyQuaternion(quaternion);

                        scope.object.up.applyQuaternion(quaternion);

                        _lastAxis.copy(axis);

                        _lastAngle = angle;

                    } else if (!scope.staticMoving && _lastAngle) {

                        _lastAngle *= Math.sqrt(1.0 - scope.dynamicDampingFactor);

                        _eye.copy(scope.object.position).sub(scope.target);

                        quaternion.setFromAxisAngle(_lastAxis, _lastAngle);

                        _eye.applyQuaternion(quaternion);

                        scope.object.up.applyQuaternion(quaternion);

                    }

                    _movePrev.copy(_moveCurr);

                };

            }();

            this.zoomCamera = function() {

                let factor;

                if (_state === STATE.TOUCH_ZOOM_PAN) {

                    factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
                    _touchZoomDistanceStart = _touchZoomDistanceEnd;

                    if (scope.object.isPerspectiveCamera) {

                        _eye.multiplyScalar(factor);

                    } else if (scope.object.isOrthographicCamera) {

                        scope.object.zoom *= factor;
                        scope.object.updateProjectionMatrix();

                    } else {

                        console.warn('THREE.TrackballControls: Unsupported camera type');

                    }

                } else {

                    factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * scope.zoomSpeed;

                    if (factor !== 1.0 && factor > 0.0) {

                        if (scope.object.isPerspectiveCamera) {

                            _eye.multiplyScalar(factor);

                        } else if (scope.object.isOrthographicCamera) {

                            scope.object.zoom /= factor;
                            scope.object.updateProjectionMatrix();

                        } else {

                            console.warn('THREE.TrackballControls: Unsupported camera type');

                        }

                    }

                    if (scope.staticMoving) {

                        _zoomStart.copy(_zoomEnd);

                    } else {

                        _zoomStart.y += (_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor;

                    }

                }

            };

            this.panCamera = function() {

                const mouseChange = new THREE.Vector2(),
                    objectUp = new THREE.Vector3(),
                    pan = new THREE.Vector3();
                return function panCamera() {

                    mouseChange.copy(_panEnd).sub(_panStart);

                    if (mouseChange.lengthSq()) {

                        if (scope.object.isOrthographicCamera) {

                            const scale_x = (scope.object.right - scope.object.left) / scope.object.zoom / scope.domElement.clientWidth;
                            const scale_y = (scope.object.top - scope.object.bottom) / scope.object.zoom / scope.domElement.clientWidth;
                            mouseChange.x *= scale_x;
                            mouseChange.y *= scale_y;

                        }

                        mouseChange.multiplyScalar(_eye.length() * scope.panSpeed);
                        pan.copy(_eye).cross(scope.object.up).setLength(mouseChange.x);
                        pan.add(objectUp.copy(scope.object.up).setLength(mouseChange.y));
                        scope.object.position.add(pan);
                        scope.target.add(pan);

                        if (scope.staticMoving) {

                            _panStart.copy(_panEnd);

                        } else {

                            _panStart.add(mouseChange.subVectors(_panEnd, _panStart).multiplyScalar(scope.dynamicDampingFactor));

                        }

                    }

                };

            }();

            this.checkDistances = function() {

                if (!scope.noZoom || !scope.noPan) {

                    if (_eye.lengthSq() > scope.maxDistance * scope.maxDistance) {

                        scope.object.position.addVectors(scope.target, _eye.setLength(scope.maxDistance));

                        _zoomStart.copy(_zoomEnd);

                    }

                    if (_eye.lengthSq() < scope.minDistance * scope.minDistance) {

                        scope.object.position.addVectors(scope.target, _eye.setLength(scope.minDistance));

                        _zoomStart.copy(_zoomEnd);

                    }

                }

            };

            this.update = function() {

                _eye.subVectors(scope.object.position, scope.target);

                if (!scope.noRotate) {

                    scope.rotateCamera();

                }

                if (!scope.noZoom) {

                    scope.zoomCamera();

                }

                if (!scope.noPan) {

                    scope.panCamera();

                }

                scope.object.position.addVectors(scope.target, _eye);

                if (scope.object.isPerspectiveCamera) {

                    scope.checkDistances();
                    scope.object.lookAt(scope.target);

                    if (lastPosition.distanceToSquared(scope.object.position) > EPS) {

                        scope.dispatchEvent(_changeEvent);
                        lastPosition.copy(scope.object.position);

                    }

                } else if (scope.object.isOrthographicCamera) {

                    scope.object.lookAt(scope.target);

                    if (lastPosition.distanceToSquared(scope.object.position) > EPS || lastZoom !== scope.object.zoom) {

                        scope.dispatchEvent(_changeEvent);
                        lastPosition.copy(scope.object.position);
                        lastZoom = scope.object.zoom;

                    }

                } else {

                    console.warn('THREE.TrackballControls: Unsupported camera type');

                }

            };

            this.reset = function() {

                _state = STATE.NONE;
                _keyState = STATE.NONE;
                scope.target.copy(scope.target0);
                scope.object.position.copy(scope.position0);
                scope.object.up.copy(scope.up0);
                scope.object.zoom = scope.zoom0;
                scope.object.updateProjectionMatrix();

                _eye.subVectors(scope.object.position, scope.target);

                scope.object.lookAt(scope.target);
                scope.dispatchEvent(_changeEvent);
                lastPosition.copy(scope.object.position);
                lastZoom = scope.object.zoom;

            }; // listeners


            function onPointerDown(event) {

                if (scope.enabled === false) return;

                if (_pointers.length === 0) {

                    scope.domElement.setPointerCapture(event.pointerId);
                    scope.domElement.addEventListener('pointermove', onPointerMove);
                    scope.domElement.addEventListener('pointerup', onPointerUp);

                } //


                addPointer(event);

                if (event.pointerType === 'touch') {

                    onTouchStart(event);

                } else {

                    onMouseDown(event);

                }

            }

            function onPointerMove(event) {

                if (scope.enabled === false) return;

                if (event.pointerType === 'touch') {

                    onTouchMove(event);

                } else {

                    onMouseMove(event);

                }

            }

            function onPointerUp(event) {

                if (scope.enabled === false) return;

                if (event.pointerType === 'touch') {

                    onTouchEnd(event);

                } else {

                    onMouseUp();

                } //


                removePointer(event);

                if (_pointers.length === 0) {

                    scope.domElement.releasePointerCapture(event.pointerId);
                    scope.domElement.removeEventListener('pointermove', onPointerMove);
                    scope.domElement.removeEventListener('pointerup', onPointerUp);

                }

            }

            function onPointerCancel(event) {

                removePointer(event);

            }

            function keydown(event) {

                if (scope.enabled === false) return;
                window.removeEventListener('keydown', keydown);

                if (_keyState !== STATE.NONE) {

                    return;

                } else if (event.code === scope.keys[STATE.ROTATE] && !scope.noRotate) {

                    _keyState = STATE.ROTATE;

                } else if (event.code === scope.keys[STATE.ZOOM] && !scope.noZoom) {

                    _keyState = STATE.ZOOM;

                } else if (event.code === scope.keys[STATE.PAN] && !scope.noPan) {

                    _keyState = STATE.PAN;

                }

            }

            function keyup() {

                if (scope.enabled === false) return;
                _keyState = STATE.NONE;
                window.addEventListener('keydown', keydown);

            }

            function onMouseDown(event) {

                if (_state === STATE.NONE) {

                    switch (event.button) {

                        case scope.mouseButtons.LEFT:
                            _state = STATE.ROTATE;
                            break;

                        case scope.mouseButtons.MIDDLE:
                            _state = STATE.ZOOM;
                            break;

                        case scope.mouseButtons.RIGHT:
                            _state = STATE.PAN;
                            break;

                        default:
                            _state = STATE.NONE;

                    }

                }

                const state = _keyState !== STATE.NONE ? _keyState : _state;

                if (state === STATE.ROTATE && !scope.noRotate) {

                    _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));

                    _movePrev.copy(_moveCurr);

                } else if (state === STATE.ZOOM && !scope.noZoom) {

                    _zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY));

                    _zoomEnd.copy(_zoomStart);

                } else if (state === STATE.PAN && !scope.noPan) {

                    _panStart.copy(getMouseOnScreen(event.pageX, event.pageY));

                    _panEnd.copy(_panStart);

                }

                scope.dispatchEvent(_startEvent);

            }

            function onMouseMove(event) {

                const state = _keyState !== STATE.NONE ? _keyState : _state;

                if (state === STATE.ROTATE && !scope.noRotate) {

                    _movePrev.copy(_moveCurr);

                    _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));

                } else if (state === STATE.ZOOM && !scope.noZoom) {

                    _zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY));

                } else if (state === STATE.PAN && !scope.noPan) {

                    _panEnd.copy(getMouseOnScreen(event.pageX, event.pageY));

                }

            }

            function onMouseUp() {

                _state = STATE.NONE;
                scope.dispatchEvent(_endEvent);

            }

            function onMouseWheel(event) {

                if (scope.enabled === false) return;
                if (scope.noZoom === true) return;
                event.preventDefault();

                switch (event.deltaMode) {

                    case 2:
                        // Zoom in pages
                        _zoomStart.y -= event.deltaY * 0.025;
                        break;

                    case 1:
                        // Zoom in lines
                        _zoomStart.y -= event.deltaY * 0.01;
                        break;

                    default:
                        // undefined, 0, assume pixels
                        _zoomStart.y -= event.deltaY * 0.00025;
                        break;

                }

                scope.dispatchEvent(_startEvent);
                scope.dispatchEvent(_endEvent);

            }

            function onTouchStart(event) {

                trackPointer(event);

                switch (_pointers.length) {

                    case 1:
                        _state = STATE.TOUCH_ROTATE;

                        _moveCurr.copy(getMouseOnCircle(_pointers[0].pageX, _pointers[0].pageY));

                        _movePrev.copy(_moveCurr);

                        break;

                    default:
                        // 2 or more
                        _state = STATE.TOUCH_ZOOM_PAN;
                        const dx = _pointers[0].pageX - _pointers[1].pageX;
                        const dy = _pointers[0].pageY - _pointers[1].pageY;
                        _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy);
                        const x = (_pointers[0].pageX + _pointers[1].pageX) / 2;
                        const y = (_pointers[0].pageY + _pointers[1].pageY) / 2;

                        _panStart.copy(getMouseOnScreen(x, y));

                        _panEnd.copy(_panStart);

                        break;

                }

                scope.dispatchEvent(_startEvent);

            }

            function onTouchMove(event) {

                trackPointer(event);

                switch (_pointers.length) {

                    case 1:
                        _movePrev.copy(_moveCurr);

                        _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));

                        break;

                    default:
                        // 2 or more
                        const position = getSecondPointerPosition(event);
                        const dx = event.pageX - position.x;
                        const dy = event.pageY - position.y;
                        _touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy);
                        const x = (event.pageX + position.x) / 2;
                        const y = (event.pageY + position.y) / 2;

                        _panEnd.copy(getMouseOnScreen(x, y));

                        break;

                }

            }

            function onTouchEnd(event) {

                switch (_pointers.length) {

                    case 0:
                        _state = STATE.NONE;
                        break;

                    case 1:
                        _state = STATE.TOUCH_ROTATE;

                        _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));

                        _movePrev.copy(_moveCurr);

                        break;

                    case 2:
                        _state = STATE.TOUCH_ZOOM_PAN;

                        _moveCurr.copy(getMouseOnCircle(event.pageX - _movePrev.pageX, event.pageY - _movePrev.pageY));

                        _movePrev.copy(_moveCurr);

                        break;

                }

                scope.dispatchEvent(_endEvent);

            }

            function contextmenu(event) {

                if (scope.enabled === false) return;
                event.preventDefault();

            }

            function addPointer(event) {

                _pointers.push(event);

            }

            function removePointer(event) {

                delete _pointerPositions[event.pointerId];

                for (let i = 0; i < _pointers.length; i++) {

                    if (_pointers[i].pointerId == event.pointerId) {

                        _pointers.splice(i, 1);

                        return;

                    }

                }

            }

            function trackPointer(event) {

                let position = _pointerPositions[event.pointerId];

                if (position === undefined) {

                    position = new THREE.Vector2();
                    _pointerPositions[event.pointerId] = position;

                }

                position.set(event.pageX, event.pageY);

            }

            function getSecondPointerPosition(event) {

                const pointer = event.pointerId === _pointers[0].pointerId ? _pointers[1] : _pointers[0];
                return _pointerPositions[pointer.pointerId];

            }

            this.dispose = function() {

                scope.domElement.removeEventListener('contextmenu', contextmenu);
                scope.domElement.removeEventListener('pointerdown', onPointerDown);
                scope.domElement.removeEventListener('pointercancel', onPointerCancel);
                scope.domElement.removeEventListener('wheel', onMouseWheel);
                scope.domElement.removeEventListener('pointermove', onPointerMove);
                scope.domElement.removeEventListener('pointerup', onPointerUp);
                window.removeEventListener('keydown', keydown);
                window.removeEventListener('keyup', keyup);

            };

            this.domElement.addEventListener('contextmenu', contextmenu);
            this.domElement.addEventListener('pointerdown', onPointerDown);
            this.domElement.addEventListener('pointercancel', onPointerCancel);
            this.domElement.addEventListener('wheel', onMouseWheel, {
                passive: false
            });
            window.addEventListener('keydown', keydown);
            window.addEventListener('keyup', keyup);
            this.handleResize(); // force an update at start

            this.update();

        }

    }

    THREE.TrackballControls = TrackballControls;

})();